fix(checkout): auth-gate /app/checkout + fail-closed razorpay_configured (BUG-P111/P112/P013/P088/P121)#146
Merged
Conversation
…red (BUG-P111/P112/P013/P088/P121)
QA found that visiting /app/checkout/?plan=hobby while unauthenticated
landed at a LIVE-mode Razorpay subscription page (sub_Sv96Mt2n8nnDYL).
A stale localStorage JWT was passing the App-level AuthGate (which only
checks getToken(), not validity), so the SPA short-circuited into a
real Razorpay create-subscription call. BUG-P111 (P0 critical).
Plus three sibling bugs from the same QA session:
- BUG-P013 (P1): /login redirect dropped the plan context, losing the
funnel after signin.
- BUG-P088 (P1): razorpay_configured defaulted to TRUE when the API
omitted the flag — clicking upgrade hit a 502 from Razorpay because
recurring is not enabled on the prod account (per CLAUDE.md memory
project_razorpay_recurring_not_enabled.md).
- BUG-P121/P122 (P1): a future caching regression of sub_* shortlinks
would survive logout without a dedicated purge.
Fixes:
1. CheckoutPage gains a SECOND-LAYER auth gate that re-checks getToken()
on mount BEFORE any createCheckout call. On miss it fails CLOSED with
window.location.assign('/login?next=<encoded path>'), preserving plan
+ frequency so post-signin returns the user to the same checkout.
2. CheckoutPage also handles the new 503 billing_misconfigured envelope
from the server-side guard (fix/billing-traffic-env-and-misconfig-
detection on the api repo) the same way as billing_not_configured —
honest fallback panel instead of a raw error banner.
3. CheckoutPage registers a logout hook that purges every localStorage
key under CHECKOUT_CACHE_KEY_PREFIX. The page does NOT cache sub_*
today; the prefix + purge guarantee is the defensive belt against
any future regression that adds such caching.
4. LoginPage now honors ?next=<encoded path> from the query string,
falling back to loc.state.from (existing AuthGate path) then /app.
Only relative same-origin paths are honoured — /login?next=https:
//evil.com and protocol-relative //evil.com both fall back to /app
to defeat open-redirect phishing.
5. mapBillingState flips razorpay_configured `?? true` → `?? false`.
When the API doesn't say, hide the upgrade button. Honest copy beats
a button that 502s.
### Surface checklist (rule 22)
- api/plans.yaml not affected
- common/plans/plans.go defaultYAML not affected
- api/internal/handlers/openapi.go updated in companion PR (api)
- instanode-web/.../PricingPage.tsx not affected (CTA wiring unchanged)
- content/llms.txt not affected
- dashboard upgradeCopy.ts not present in this repo
Tests added (all 1069 vitest pass):
- TestCheckoutPage_UnauthRedirectsToLogin
- TestCheckoutPage_PreservesNextParam
- TestCheckoutPage_PreservesFrequencyMonthlyWhenOnlyPlan
- TestCheckoutPage_FlipsToUnauthOnMidFlight401
- TestCheckoutPage_ClearsCachedSubOnLogout (clearCheckoutCache suite)
- TestCheckoutPage_RendersFallbackOn503BillingMisconfigured (BUG-P112)
- LoginPage_HonorsNextRoundTripsToCheckout (BUG-P013)
- LoginPage_RejectsAbsoluteEvilCom (open-redirect)
- LoginPage_RejectsProtocolRelativeNext (open-redirect)
- TestFetchBilling_DefaultsRazorpayConfiguredFalse (BUG-P088 fail-closed)
Live verification queued in companion api PR — see body for curl + Chrome MCP evidence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
size-limit report 📦
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
QA found that visiting
/app/checkout/?plan=hobbywhile unauthenticatedlanded at a LIVE-mode Razorpay subscription page (
sub_Sv96Mt2n8nnDYL).A stale localStorage JWT was passing the App-level AuthGate, so the SPA
short-circuited into a real Razorpay create-subscription call.
BUG-P111 (P0 critical).
Plus three sibling bugs from the same QA session:
/logindropped the plan context, losing the funnel after signin.razorpay_configureddefaulted to TRUE when the API omitted the flag — clicking upgrade hit a 502 from Razorpay because recurring is not enabled on the prod account (per CLAUDE.md memoryproject_razorpay_recurring_not_enabled.md).sub_*shortlinks would survive logout without a dedicated purge.Fixes
CheckoutPagegains a second-layer auth gate that re-checksgetToken()on mount before anycreateCheckoutcall. On miss →window.location.assign('/login?next=<encoded path>'). Preserves plan + frequency so post-signin returns the user to the same checkout.CheckoutPagehandles the new 503billing_misconfiguredenvelope (from the companion api PR's server-side guard) the same way asbilling_not_configured— friendly fallback panel, no live Razorpay URL.CheckoutPageregisters a logout hook that purges everylocalStoragekey underCHECKOUT_CACHE_KEY_PREFIX. The page does NOT cachesub_*today; the prefix + purge guarantee is the defensive belt against any future caching regression.LoginPagehonors?next=<encoded path>from the query string, falling back toloc.state.fromthen/app. Only relative same-origin paths are honoured —/login?next=https://evil.comand protocol-relative//evil.comboth fall back to/appto defeat open-redirect phishing.mapBillingStateflipsrazorpay_configured ?? true → ?? false. When the API doesn't say, hide the upgrade button. Honest copy beats a button that 502s.Surface checklist (rule 22)
api/plans.yamlcommon/plans/plans.godefaultYAMLapi/internal/handlers/openapi.gofix/billing-traffic-env-and-misconfig-detection)instanode-web/PricingPage.tsxcontent/llms.txtTests added
All 1069 vitest tests pass (76 files):
TestCheckoutPage_UnauthRedirectsToLoginTestCheckoutPage_PreservesNextParamTestCheckoutPage_PreservesFrequencyMonthlyWhenOnlyPlanTestCheckoutPage_FlipsToUnauthOnMidFlight401TestCheckoutPage_ClearsCachedSubOnLogout(clearCheckoutCachesuite)TestCheckoutPage_RendersFallbackOn503BillingMisconfigured(BUG-P112 server-side guard handshake)TestLoginPage_HonorsNextRoundTripsToCheckout(BUG-P013)TestLoginPage_RejectsAbsoluteEvilCom(open-redirect)TestLoginPage_RejectsProtocolRelativeNext(open-redirect)TestFetchBilling_DefaultsRazorpayConfiguredFalse(BUG-P088 fail-closed)Live verification (rule 14)
Queued for after merge & GH Pages deploy. Per brief:
curl https://instanode.dev/app/checkout/?plan=hobby | grep -c "rzp_live"should be 0./login?next=/app/checkout?...(Chrome MCP screenshot to follow).curl /api/v1/billing/checkoutresponse includestraffic_env(verified via companion api PR).Risk
The auth-gate change adds a redundant pre-flight check that the App-level
AuthGatealready covers for the no-token case — but the brief is specifically about the stale-JWT case where the App gate passes and the server 401s. Mid-flight 401 path taggedunauthenticated. Logged-in upgrade flow tests still pass (the redirect happens BEFORE the auth gate in the effect, and the existinggetToken()returns'test-token'by default in the test mock).🤖 Generated with Claude Code